package main

import (
	"sort"
	"time"
)

var _Cron = newCron()

type byTime []*task

func (s byTime) Len() int      { return len(s) }
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byTime) Less(i, j int) bool {
	if s[i].next.IsZero() {
		return false
	}
	if s[j].next.IsZero() {
		return true
	}
	return s[i].next.Before(s[j].next)
}

type cron struct {
	tasks   []*task
	indexes map[string]int
	running bool

	stop    chan struct{}
	addChan chan *task
	delChan chan string
}

func newCron() *cron {
	return &cron{
		tasks:   nil,
		indexes: make(map[string]int),
		running: false,

		addChan: make(chan *task),
		delChan: make(chan string),
		stop:    make(chan struct{}),
	}
}

func (c *cron) run() {
	now := time.Now()
	for _, t := range c.tasks {
		t.touch(now)
	}

	timer := time.NewTimer(time.Minute)
	for {
		sort.Sort(byTime(c.tasks))
		c.reIndex()

		var effective time.Time
		if len(c.tasks) == 0 || c.tasks[0].next.IsZero() {
			// If there are no tasks yet, just sleep - it still handles new tasks
			effective = now.AddDate(10, 0, 0)
		} else {
			effective = c.tasks[0].next
		}

		timer.Reset(effective.Sub(now))
		select {
		case now = <-timer.C:
			for _, t := range c.tasks {
				if t.next != effective {
					break
				}
				go t.run()
				t.touch(now)
			}
			continue

		case newTask := <-c.addChan:
			if index, ok := c.indexes[newTask.ID]; ok {
				c.tasks[index] = newTask
			} else {
				c.tasks, c.indexes[newTask.ID] = append(c.tasks, newTask), len(c.tasks)
			}
			newTask.touch(now)

		case id := <-c.delChan:
			c.del(id)
		case <-c.stop:
			timer.Stop()
			return
		}

		now = time.Now()
	}
}

func (c *cron) add(t *task) {
	if c.running {
		c.addChan <- t
		return
	}

	if index, ok := c.indexes[t.ID]; ok {
		c.tasks[index] = t
		return
	}
	c.tasks, c.indexes[t.ID] = append(c.tasks, t), len(c.tasks)
}

func (c *cron) del(id string) {
	index, ok := c.indexes[id]
	if !ok {
		return
	}

	if c.running {
		c.delChan <- id
		return
	}

	c.tasks = append(c.tasks[:index], c.tasks[index+1:]...)
	delete(c.indexes, id)
	return
}

// Rebuild the indexes
func (c *cron) reIndex() {
	for i, count := 0, len(c.tasks); i < count; i++ {
		c.indexes[c.tasks[i].ID] = i
	}
}
